//Copyright (c) 2009, Offbeat Guides
//All rights reserved.
//
//Redistribution and use in source and binary forms, with or without modification, are 
//permitted provided that the following conditions are met:
//
//* Redistributions of source code must retain the above copyright notice, this list of 
//conditions and the following disclaimer.
//
//* Redistributions in binary form must reproduce the above copyright notice, this list 
//of conditions and the following disclaimer in the documentation and/or other materials 
//provided with the distribution.
//
//* Neither the name of Offbeat Guides nor the names of its contributors may be used to 
//endorse or promote products derived from this software without specific prior written 
//permission.
//
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
//EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
//OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
//SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
//SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 
//OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
//HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
//TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
//EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
using System.Xml;

namespace Amazon.Services
{
    internal class ServiceClient
    {
        #region Static Variables
        protected static System.Text.Encoding Encoding;
        private static System.Xml.Serialization.XmlSerializer ErrorResponseSerializer;
        private static System.Collections.Generic.Dictionary<Type, XmlSerializer> Serializers;
        #endregion

        #region Properties
        public AmazonAccount Account { get; set; }

        public IServiceConfiguration Configuration { get; set; }
        #endregion

        #region Constructors and Finalizers
        static ServiceClient()
        {
            ServiceClient.Encoding = new System.Text.UTF8Encoding();
            ServiceClient.Serializers = new Dictionary<Type, XmlSerializer>();
            ServiceClient.ErrorResponseSerializer = new XmlSerializer(typeof(ErrorResponse));
        }

        /// <summary>
        /// Constructs AmazonClient with Amazon account
        /// </summary>
        /// <param name="account">AWS Account</param>
        /// <param name="config">configuration</param>
        public ServiceClient(AmazonAccount account, IServiceConfiguration config)
        {
            this.Account = account;
            this.Configuration = config;

            ServicePointManager.Expect100Continue = false;
            ServicePointManager.UseNagleAlgorithm = false;
        }
        #endregion

        #region Methods
        protected virtual WebRequest CreateRequest(IDictionary<String, String> parameters, Byte[] contents)
        {
            return this.Configuration.CreateRequest(this.Account, parameters, contents);
        }

        protected virtual void HandleExeption(WebException ex)
        {
            String responseData = String.Empty;
            HttpStatusCode status = default(HttpStatusCode);

            using (HttpWebResponse response = (ex as WebException).Response as HttpWebResponse)
            {
                if (response == null)
                    throw new AmazonException(ex);

                status = response.StatusCode;
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                    responseData = reader.ReadToEnd();
            }

            if (status != HttpStatusCode.InternalServerError && status != HttpStatusCode.ServiceUnavailable)
            {
                try
                {
                    using (StringReader reader = new StringReader(responseData))
                    {
                        XmlTextReader xmlReader = new XmlTextReader(reader);
                        if (String.IsNullOrEmpty(responseData) || !ServiceClient.ErrorResponseSerializer.CanDeserialize(xmlReader))
                            throw new AmazonException(ex, status);

                        // throw formatted exception with information available from the error response
                        ErrorResponse errorResponse = ServiceClient.ErrorResponseSerializer.Deserialize(reader) as ErrorResponse;
                        throw new AmazonException(errorResponse.RequestId, errorResponse.Message, status);
                    }
                }
                catch (Exception e)
                {
                    // try a last ditch effort to figure out the error messages
                    if (!(e is AmazonException))
                        e = this.ReportAnyErrors(responseData, status, e);

                    throw;
                }
            }
        }

        private AmazonException ReportAnyErrors(String responseBody, HttpStatusCode status, Exception e)
        {
            if (responseBody != null && responseBody.StartsWith("<"))
            {
                Match errorMatcherOne = Regex.Match(responseBody, Properties.Resources.Error1Regex, RegexOptions.Multiline);
                Match errorMatcherTwo = Regex.Match(responseBody, Properties.Resources.Error2Regex, RegexOptions.Multiline);

                if (errorMatcherOne.Success || errorMatcherTwo.Success)
                {

                    String requestId = errorMatcherOne.Groups["requestId"].Value;
                    String code = errorMatcherOne.Groups["code"].Value;
                    String message = errorMatcherOne.Groups["message"].Value;
                    return new AmazonException(requestId, message, status);
                }
            }

            return new AmazonException("Internal Error", status);
        }

        public Response Invoke(IDictionary<String, String> parameters)
        {
            return this.Invoke(parameters, null);
        }

        public Response Invoke(IDictionary<String, String> parameters, Byte[] contents)
        {
            // attempt to make the request a number of times if it fails with valid errors
            for (int retries = 0; retries < this.Configuration.MaxErrorRetry; retries++)
            {
                try
                {
                    WebRequest request = this.CreateRequest(parameters, contents);

                    // get the response stream and return a generalized response object
                    using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
                        return new Response(response);
                }
                catch (WebException ex)
                {
                    this.HandleExeption(ex);

                    // retry again but wait a bit before doing so
                    System.Threading.Thread.Sleep((Int32)Math.Pow(4, retries) * 100);
                }
            }

            // we have reached the max number of requests and popped out of the loop
            throw new AmazonException("Maximum number of retry attempts reached.");
        }
        #endregion
    }
}